---
title: 访问者模式
description: Visitor Pattern - 表示一个作用于某对象结构中的各元素的操作，它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作
sidebar_position: 9
---

## 定义
访问者模式在实际的开发中使用的非常少，因为它比较难以实现，并且应用该模式可能会导致代码的可读性变差，在没有必要时，不建议使用访问者模式。

**访问者模式（Visitor pattern）的原始定义时：允许在运行时将一个或多个操作应用于一组对象，将操作与对象结构分离。**

* 一个是：运行时使用一组对象的一个或多个操作，比如，对不同类型的文件（.pdf、.xml、.properties) 进行扫描；
* 另一个是：分离对象的操作和对象本身的结构，比如，扫描多个文件夹下的多个文件，对于文件来说，扫描是额外的业务操作，如果在每个文件对象上都加一个扫描操作，太过于冗余，而扫描操作具有统一性，非常适合访问者模式。

访问者模式主要解决的是数据与算法的耦合问题，尤其是在数据结构比较稳定，而算法多变的情况下，为了不污染数据本身，访问者会将多种算法独立归档，并在访问数据时根据数据类型自动切换到对应的算法，实现数据的自动响应机制，并确保算法的自由扩展。

## 模式原理
* **Visitor 接口**：访问者接口，用于处理元素对象，参数也是元素接口
* **Element 接口**：元素接口，包含 accept 接口，接受 Visitor 参数。目的是想让每一个元素都可以让访问者去访问到。
* **ConcreteElement 类**：具体的元素角色，实现 Element 接口，并实现 accept 方法，该方法会调用访问者对象的 visit 方法。
* **ConcreteVisitor 类**：具体的访问者角色，实现 Visitor 角色，并实现 visit 方法，该方法会处理元素对象。
* **Client**：初始化各种数据元素类，并选择合适的访问者处理容器中的所有数据对象。
* **ObjectStructure**：对象结构，管理元素集合，提供遍历它的方法

![访问者模式类图](https://wkq-img.oss-cn-chengdu.aliyuncs.com/site/20250831175822.png)

## 模式实现

### 场景描述
以超市购物为例，假设超市中的三类商品：水果、糖果、酒水进行售卖。我们可以忽略每种商品的计价方法，因为最终结账由收银员统一集中处理，在商品类添加计价方法是不合理的设计。

### 核心接口定义

#### 1. 访问者接口（Visitor）
```java
/**
 * 访问者接口
 * 根据入参的不同，调用对应的重载方法
 */
public interface Visitor {
    void visit(Candy candy); // 糖果重载方法
    void visit(Wine wine);   // 酒水重载方法
    void visit(Fruit fruit); // 水果重载方法
}
```

#### 2. 接待者接口（Acceptable）
```java
/**
 * 接待者接口（抽象元素角色）
 */
public interface Acceptable {
    // 接受所有的 Visitor 访问者的子类
    void accept(Visitor visitor);
}
```

### 抽象基类

#### 3. 抽象商品类（Product）
```java
/**
 * 抽象商品类
 */
public abstract class Product {
    private String name;           // 商品名
    private LocalDate producedDate; // 生产日期
    private double price;          // 单品价格

    public Product(String name, LocalDate producedDate, double price) {
        this.name = name;
        this.producedDate = producedDate;
        this.price = price;
    }

    // getter 和 setter 方法
}
```

### 具体元素类

#### 4. 糖果类（Candy）
```java
/**
 * 糖果类
 */
public class Candy extends Product implements Acceptable {
    public Candy(String name, LocalDate producedDate, double price) {
        super(name, producedDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
```

#### 5. 酒水类（Wine）
```java
/**
 * 酒水类
 */
public class Wine extends Product implements Acceptable {
    public Wine(String name, LocalDate producedDate, double price) {
        super(name, producedDate, price);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
```

#### 6. 水果类（Fruit）
```java
/**
 * 水果类
 */
public class Fruit extends Product implements Acceptable {
    private double weight;

    public Fruit(String name, LocalDate producedDate, double price, double weight) {
        super(name, producedDate, price);
        this.weight = weight;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
```

### 具体访问者类

#### 7. 折扣计价访问者（DiscountVisitor）
```java
import java.text.NumberFormat;
import java.time.LocalDate;

/**
 * 折扣计价访问者类
 */
public class DiscountVisitor implements Visitor {
    private LocalDate billDate;

    public DiscountVisitor(LocalDate billDate) {
        this.billDate = billDate;
        System.out.println("结算日期：" + billDate);
    }

    @Override
    public void visit(Candy candy) {
        System.out.println("糖果：" + candy.getName());
        // 糖果大于 180 天，禁止售卖，否则糖果一律九折
        long days = billDate.toEpochDay() - candy.getProducedDate().toEpochDay();
        if (days > 180) {
            System.out.println("超过半年的糖果，请勿使用！");
        } else {
            double realPrice = candy.getPrice() * 0.9;
            System.out.println("糖果打折后的价格为：" + NumberFormat.getCurrencyInstance().format(realPrice));
        }
    }

    @Override
    public void visit(Wine wine) {
        System.out.println("酒类" + wine.getName() + "无折扣价格！");
        System.out.println("原价售卖：" + NumberFormat.getCurrencyInstance().format(wine.getPrice()));
    }

    @Override
    public void visit(Fruit fruit) {
        System.out.println("水果:" + fruit.getName());
        long days = billDate.toEpochDay() - fruit.getProducedDate().toEpochDay();
        double rate = 0;
        if (days > 7) {
            System.out.println("超过 7 天的水果，请勿食用！");
        } else if (days > 3) {
            rate = 0.5;
        } else {
            rate = 1;
        }
        double realPrice = fruit.getPrice() * fruit.getWeight() * rate;
        System.out.println("水果价格为：" + NumberFormat.getCurrencyInstance().format(realPrice));
    }
}
```

### 客户端测试

#### 8. 客户端类（Client）
```java
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import org.junit.Test;

public class Client {
    
    @Test
    public void test1() {
        Candy candy = new Candy("德芙巧克力", LocalDate.of(2022, 5, 1), 10.0);
        Visitor visitor = new DiscountVisitor(LocalDate.of(2022, 10, 5));
        visitor.visit(candy);
    }
    
    /**
     * 模拟结算三件商品（错误示例）
     */
    @Test
    public void test2() {
        List<Product> products = Arrays.asList(
            new Candy("金丝猴奶糖", LocalDate.of(2022, 10, 1), 10),
            new Wine("郎酒", LocalDate.of(2022, 10, 1), 1000),
            new Fruit("草莓", LocalDate.of(2022, 10, 8), 50, 2)
        );

        // 创建 Visitor
        DiscountVisitor visitor = new DiscountVisitor(LocalDate.of(2022, 10, 5));

        // 这种方式无法调用 accept 方法，因为 Product 没有实现 Acceptable 接口
        for (Product product : products) {
            // product.accept(visitor); // 编译错误
        }
    }
    
    /**
     * 模拟结算三件商品（正确示例）
     */
    @Test
    public void test3() {
        List<Acceptable> products = Arrays.asList(
            new Candy("金丝猴奶糖", LocalDate.of(2022, 10, 1), 10),
            new Wine("郎酒", LocalDate.of(2022, 10, 1), 1000),
            new Fruit("草莓", LocalDate.of(2022, 10, 8), 50, 2)
        );

        // 创建 Visitor
        DiscountVisitor visitor = new DiscountVisitor(LocalDate.of(2022, 10, 5));

        // 正确使用访问者模式
        for (Acceptable product : products) {
            product.accept(visitor);
        }
    }
}
```

### 运行结果示例
```
结算日期：2022-10-05
糖果：金丝猴奶糖
糖果打折后的价格为：￥9.00
酒类郎酒无折扣价格！
原价售卖：￥1,000.00
水果:草莓
水果价格为：￥100.00
```

## 模式分析

### 核心机制
1. **双重分发**：访问者模式使用了双重分发机制
   - 第一次分发：客户端调用 `product.accept(visitor)`
   - 第二次分发：`accept` 方法调用 `visitor.visit(this)`

2. **类型安全**：通过方法重载确保类型安全，每种商品类型都有对应的 `visit` 方法

3. **开闭原则**：新增访问者操作时，不需要修改现有的元素类

### 设计要点
1. **元素类职责单一**：元素类只负责数据存储和基本的 `accept` 方法
2. **访问者类职责明确**：每个访问者类负责特定的业务逻辑
3. **接口隔离**：通过 `Acceptable` 接口实现元素与访问者的解耦

## 总结

### 优点
* **扩展性好**：在不改变对象结构中元素的情况下，为对象结构中的元素添加新的功能。
* **复用性好**：通过访问者来定义整个对象结构通用的功能，从而提高复用性。
* **分离无关行为**：通过访问者来分离无关的行为，把相关的行为封装在一起，构成一个访问者，这样每一个访问者的功能都比较单一。
* **类型安全**：通过方法重载确保编译时类型检查。

### 缺点
* **对象结构变化很困难**：在访问者模式中，每增加一个新的元素类，都要在每一个具体访问者中增加相应的具体操作，这违背了开闭原则。
* **违反了依赖倒置原则**：访问者模式依赖具体类，而没有依赖抽象类。
* **增加系统复杂度**：引入了额外的接口和类，增加了系统的复杂度。
* **破坏封装性**：访问者需要访问元素的内部状态，可能破坏封装性。

### 使用场景
* **当对象的数据结构相对稳定，而操作却经常变化的时候**。比如，路由器本身的内部构造（数据结构）不会怎么变化，但是在不同的操作系统下的操作可能会经常变化，比如，发送数据、接受数据等。

* **需要将数据结构与不常用的操作进行分离的时候**。比如，扫描文件内容这个动作通常不是文件常用的操作，但是对于文件夹和文件来说，和数据结构本身没有太大关系（树形结构的遍历操作），扫描是一个额外的动作，如果给每个文件都添加一个扫描操作会太过于重复，这时采用访问者模式是非常合适的，能够很好分离文件自身的遍历操作和外部的扫描操作。

* **需要在运行时动态决定使用那些对象和方法的时候**。比如，对于监控系统来说，很多时候需要监控运行时的程序状态，但大多数时候又无法预知对象编译时的状态和参数，这时使用访问者模式就可以动态增加监控行为。

* **需要对复杂对象结构进行多种不同操作时**。比如，编译器对抽象语法树进行语法检查、代码生成、优化等不同操作。

### 最佳实践
1. **谨慎使用**：访问者模式比较复杂，只有在确实需要时才使用
2. **保持简单**：避免在访问者中放置过多的业务逻辑
3. **考虑替代方案**：优先考虑策略模式、命令模式等更简单的替代方案
4. **文档化**：详细记录访问者模式的实现和使用方式，便于维护
